﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
using System.Reflection;
using System.Xml.Serialization;
using System.Collections;
using System.Runtime.Serialization.Formatters.Binary;
using System.Runtime.Serialization;
using System.Data;
using System.Xml;

namespace BinaryStructProject
{
    public class BinaryStructFormatter
    {
        public byte[] Serialize<T>(T sampleModel)
        {
            using (MemoryStream ms = new MemoryStream())
            {
                Serialize(ms, sampleModel);
                return ms.ToArray();
            }
        }

        public void Serialize(Stream stream, object data)
        {
            if (data == null)
                throw new ArgumentNullException("data");
            using (BinaryWriter bw = new BinaryWriter(stream))
            {
                Type type = data.GetType();
                Write(bw, data, type);
            }
        }

        private void Write(BinaryWriter writter, object graph, Type type)
        {
            if (type == typeof(bool))
            {
                writter.Write((bool)graph);
            }
            else if (type == typeof(byte))
            {
                writter.Write((byte)graph);
            }
            else if (type == typeof(int))
            {
                writter.Write((int)graph);
            }
            else if (type == typeof(long))
            {
                writter.Write((long)graph);
            }
            else if (type == typeof(string))
            {
                string str = graph as string;
                if (str != null)
                {
                    writter.Write(str.Length);
                    writter.Write(str);
                }
                else
                {
                    writter.Write(-1);
                }
            }
            else if (type == typeof(char))
            {
                writter.Write((char)graph);
            }
            else if (type == typeof(short))
            {
                writter.Write((short)graph);
            }
            else if (type == typeof(double))
            {
                writter.Write((double)graph);
            }
            else if (type == typeof(float))
            {
                writter.Write((float)graph);
            }
            else if (type == typeof(DateTime))
            {
                DateTime dateTime = (DateTime)graph;
                writter.Write(dateTime.Ticks);
            }
            else if (type == typeof(Guid))
            {
                Guid guid = (Guid)graph;
                writter.Write(guid.ToByteArray());
            }
            else if (type == typeof(TimeSpan))
            {
                TimeSpan timeSpan = (TimeSpan)graph;
                writter.Write(timeSpan.Ticks);
            }
            else if (type == typeof(decimal))
            {
                writter.Write((decimal)graph);
            }
            else if (type.IsArray)
            {
                Array array = (Array)graph;
                if (array != null)
                {
                    writter.Write(array.Length);
                    Type elementType = type.GetElementType();
                    foreach (var el in array)
                        Write(writter, el, elementType);
                }
                else
                {
                    writter.Write(-1);
                }
            }
            else if ((typeof(IList).IsAssignableFrom(type)))
            {
                if (graph == null)
                    writter.Write(-1);
                else
                    writter.Write(1);
                if (graph != null)
                {
                    IList list = (IList)graph;
                    writter.Write(list.Count);
                    if (type.IsGenericType)
                    {
                        Type genericArguments = type.GetGenericArguments().First();
                        for (int i = 0; i < list.Count; i++)
                        {
                            Write(writter, list[i], genericArguments);
                        }
                    }
                    else
                    {
                        for (int i = 0; i < list.Count; i++)
                        {
                            Write(writter, list[i], typeof(object));
                        }
                    }
                }
            }
            else if (type.IsEnum)
            {
                Write(writter, graph, Enum.GetUnderlyingType(type));
            }
            else if (type == typeof(object))
            {
                if (graph == null)
                {
                    writter.Write(-1);
                }
                else
                {
                    using (MemoryStream ms = new MemoryStream())
                    {
                        BinFormatter.Serialize(ms, graph);
                        writter.Write((int)ms.Length);
                        writter.Write(ms.GetBuffer(), 0, (int)ms.Length);
                    }
                }
            }
            else if (type == typeof(DataTable))
            {
                if (graph != null)
                {
                    writter.Write(1);
                    DataTable dataTable = (DataTable)graph;
                    Write(writter, dataTable.TableName, typeof(string));
                    writter.Write(dataTable.Columns.Count);
                    foreach (DataColumn col in dataTable.Columns)
                    {
                        Write(writter, GetObjectHandleByType(col.DataType), typeof(ObjectHandle));
                        Write(writter, col.ColumnName, typeof(string));
                    }
                    writter.Write(dataTable.Rows.Count);
                    foreach (DataRow row in dataTable.Rows)
                    {
                        foreach (DataColumn col in dataTable.Columns)
                        {
                            if (row.IsNull(col))
                            {
                                writter.Write(-1);
                            }
                            else
                            {
                                writter.Write(1);
                                Write(writter, row[col], col.DataType);
                            }
                        }
                    }
                }
                else
                {
                    writter.Write(-1);
                }
            }
            else if (type == typeof(XmlAttribute))
            {
                if (graph != null)
                {
                    writter.Write(1);
                    XmlAttribute xatr = (XmlAttribute)graph;
                    Write(writter, xatr.Name, typeof(string));
                    Write(writter, xatr.NamespaceURI, typeof(string));
                    Write(writter, xatr.Prefix, typeof(string));
                    Write(writter, xatr.Value, typeof(string));
                }
                else
                {
                    writter.Write(-1);
                }
            }
            else
            {
                if (graph == null)
                    writter.Write(-1);
                else
                    writter.Write(1);
                // Составной тип
                if (graph != null)
                {
                    {
                        var fields = type.GetFields(BindingFlags.Public | BindingFlags.Instance);
                        if (fields != null)
                        {
                            foreach (var field in fields.Where(x => !IsIgnoredField(x)))
                            {
                                Write(writter, field.GetValue(graph), field.FieldType);
                            }
                        }
                    }
                    {
                        var properties = type.GetProperties(BindingFlags.Public | BindingFlags.Instance);
                        if (properties != null)
                        {
                            foreach (var prop in properties.Where(x => !IsIgnoredProperty(x)))
                            {
                                Write(writter, prop.GetValue(graph, new object[0]), prop.PropertyType);
                            }
                        }
                    }
                }
            }
        }

        private bool IsIgnoredField(FieldInfo fieldInfo)
        {
            if (ContainsExcludeFromSerializationAttribute(fieldInfo))
            {
                return true;
            }

            return false;
        }

        private bool IsIgnoredProperty(PropertyInfo ptyInfo)
        {
            if (ContainsExcludeFromSerializationAttribute(ptyInfo))
            {
                return true;
            }

            if (!ptyInfo.CanRead || !ptyInfo.CanWrite)
            {
                return true;
            }

            var indexParameters = ptyInfo.GetIndexParameters();
            if (indexParameters.Length > 0)
            {
                // Indexer
                return true;
            }

            return false;
        }

        private bool ContainsExcludeFromSerializationAttribute(MemberInfo ptyInfo)
        {
            object[] attributes = ptyInfo.GetCustomAttributes(typeof(XmlIgnoreAttribute), false);
            return attributes.Length > 0;
        }

        public T Deserialize<T>(byte[] serializedData)
        {
            using (MemoryStream ms = new MemoryStream(serializedData))
            {
                return (T)Deserialize(ms, typeof(T));
            }
        }

        public object Deserialize(Stream stream, Type type)
        {
            using (BinaryReader br = new BinaryReader(stream))
            {
                return Read(br, type);
            }
        }

        private object Read(BinaryReader br, Type type)
        {
            if (type == typeof(bool))
            {
                return br.ReadBoolean();
            }
            else if (type == typeof(byte))
            {
                return br.ReadByte();
            }
            else if (type == typeof(int))
            {
                return br.ReadInt32();
            }
            else if (type == typeof(long))
            {
                return br.ReadInt64();
            }
            else if (type == typeof(string))
            {
                int len = br.ReadInt32();
                if (len < 0)
                    return null;
                return br.ReadString();
            }
            else if (type == typeof(char))
            {
                return br.ReadChar();
            }
            else if (type == typeof(short))
            {
                return br.ReadInt16();
            }
            else if (type == typeof(double))
            {
                return br.ReadDouble();
            }
            else if (type == typeof(float))
            {
                return br.ReadSingle();
            }
            else if (type == typeof(DateTime))
            {
                long ticks = br.ReadInt64();
                return new DateTime(ticks);
            }
            else if (type == typeof(Guid))
            {
                byte[] bytes = br.ReadBytes(16);
                return new Guid(bytes);
            }
            else if (type == typeof(TimeSpan))
            {
                long ticks = br.ReadInt64();
                return new TimeSpan(ticks);
            }
            else if (type == typeof(decimal))
            {
                return br.ReadDecimal();
            }
            else if (type.IsArray)
            {
                int len = br.ReadInt32();
                if (len < 0)
                    return null;
                Type elementType = type.GetElementType();
                Array array = Array.CreateInstance(elementType, len);
                for (int i = 0; i < len; i++)
                {
                    object val = Read(br, elementType);
                    array.SetValue(val, i);
                }
                return array;
            }
            else if ((typeof(IList).IsAssignableFrom(type)))
            {
                int flag = br.ReadInt32();
                if (flag < 0)
                    return null;

                IList list = (IList)Activator.CreateInstance(type);
                int len = br.ReadInt32();

                if (type.IsGenericType)
                {
                    for (int i = 0; i < len; i++)
                    {
                        object val = Read(br, type.GetGenericArguments().First());
                        list.Add(val);
                    }
                }
                else
                {
                    for (int i = 0; i < len; i++)
                    {
                        object val = Read(br, typeof(object));
                        list.Add(val);
                    }
                }
                return list;
            }
            else if (type.IsEnum)
            {
                return Read(br, Enum.GetUnderlyingType(type));
            }
            else if (type == typeof(object))
            {
                int len = br.ReadInt32();
                if (len == -1)
                    return null;
                byte[] bytes = br.ReadBytes(len);
                using (MemoryStream ms = new MemoryStream(bytes))
                {
                    return BinFormatter.Deserialize(ms);
                }
            }
            else if (type == typeof(DataTable))
            {
                int flag = br.ReadInt32();
                if (flag < 0)
                    return null;
                DataTable dataTable = new DataTable();
                dataTable.TableName = (string)Read(br, typeof(string));
                int columnsCount = br.ReadInt32();
                for (int i = 0; i < columnsCount; i++)
                {
                    DataColumn col = new DataColumn();
                    ObjectHandle objectHandle = (ObjectHandle)Read(br, typeof(ObjectHandle));
                    col.DataType = GetTypeByObjectHandle(objectHandle);
                    col.ColumnName = (string)Read(br, typeof(string));
                    dataTable.Columns.Add(col);
                }
                int rowsCount = br.ReadInt32();
                for (int i = 0; i < rowsCount; i++)
                {
                    DataRow row = dataTable.NewRow();
                    foreach (DataColumn col in dataTable.Columns)
                    {
                        int f = br.ReadInt32();
                        if (f < 0)
                            row[col] = DBNull.Value;
                        else
                            row[col] = Read(br, col.DataType);
                    }
                    dataTable.Rows.Add(row);
                }
                return dataTable;
            }
            else if (type == typeof(XmlAttribute))
            {
                int flag = br.ReadInt32();
                if (flag < 0)
                    return null;
                string name = (string)Read(br, typeof(string));
                string namespaceUri = (string)Read(br, typeof(string));
                string prefix = (string)Read(br, typeof(string));
                string value = (string)Read(br, typeof(string));

                XmlDocument xdoc = new XmlDocument();
                XmlAttribute xatr = xdoc.CreateAttribute(prefix, name, namespaceUri);
                xatr.Value = value;
                return xatr;
            }
            else
            {
                // Составной объект
                int flag = br.ReadInt32();
                if (flag < 0)
                    return null;
                object obj = Activator.CreateInstance(type);
                var fields = type.GetFields(BindingFlags.Public | BindingFlags.Instance);
                if (fields != null)
                {
                    foreach (var field in fields)
                    {
                        object val = Read(br, field.FieldType);
                        field.SetValue(obj, val);
                    }
                }
                var properties = type.GetProperties(BindingFlags.Public | BindingFlags.Instance);
                if (properties != null)
                {
                    foreach (var prop in properties)
                    {
                        object val = Read(br, prop.PropertyType);
                        prop.SetValue(obj, val, new object[0]);
                    }
                }
                return obj;
            }
        }

        public BinaryFormatter BinFormatter
        {
            get
            {
                if (FBinaryFormatter == null)
                    FBinaryFormatter = new BinaryFormatter();
                return FBinaryFormatter;
            }
        }
        BinaryFormatter FBinaryFormatter = null;

        private Type GetTypeByObjectHandle(ObjectHandle objectHandle)
        {
            switch (objectHandle)
            {
                case ObjectHandle.Null:
                    throw new ApplicationException();
                case ObjectHandle.DBNull:
                    throw new ApplicationException();
                case ObjectHandle.Zero:
                    throw new ApplicationException();
                case ObjectHandle.Boolean:
                    return typeof(bool);
                case ObjectHandle.Byte:
                    return typeof(byte);
                case ObjectHandle.Char:
                    return typeof(char);
                case ObjectHandle.Decimal:
                    return typeof(decimal);
                case ObjectHandle.Double:
                    return typeof(double);
                case ObjectHandle.Int16:
                    return typeof(short);
                case ObjectHandle.Int32:
                    return typeof(int);
                case ObjectHandle.Int64:
                    return typeof(long);
                case ObjectHandle.SByte:
                    return typeof(sbyte);
                case ObjectHandle.Single:
                    return typeof(float);
                case ObjectHandle.String:
                    return typeof(string);
                case ObjectHandle.UInt16:
                    return typeof(ushort);
                case ObjectHandle.UInt32:
                    return typeof(uint);
                case ObjectHandle.UInt64:
                    return typeof(ulong);
                case ObjectHandle.Array:
                    throw new ApplicationException();
                case ObjectHandle.List:
                    throw new ApplicationException();
                case ObjectHandle.ListGeneric:
                    throw new ApplicationException();
                case ObjectHandle.ByteArray:
                    return typeof(byte[]);
                case ObjectHandle.TimeSpan:
                    return typeof(TimeSpan);
                case ObjectHandle.DateTime:
                    return typeof(DateTime);
                case ObjectHandle.Guid:
                    return typeof(Guid);
                case ObjectHandle.Enum:
                    throw new ApplicationException();
                case ObjectHandle.Unknown:
                    throw new ApplicationException();
                default:
                    throw new ApplicationException();
            }
        }

        private ObjectHandle GetObjectHandleByType(Type handleType)
        {
            if (handleType == typeof(bool))
            {
                return ObjectHandle.Boolean;
            }
            else if (handleType == typeof(byte))
            {
                return ObjectHandle.Byte;
            }
            else if (handleType == typeof(int))
            {
                return ObjectHandle.Int32;
            }
            else if (handleType == typeof(long))
            {
                return ObjectHandle.Int64;
            }
            else if (handleType == typeof(string))
            {
                return ObjectHandle.String;
            }
            else if (handleType == typeof(char))
            {
                return ObjectHandle.Char;
            }
            else if (handleType == typeof(short))
            {
                return ObjectHandle.Int16;
            }
            else if (handleType == typeof(double))
            {
                return ObjectHandle.Double;
            }
            else if (handleType == typeof(float))
            {
                return ObjectHandle.Single;
            }
            else if (handleType == typeof(DateTime))
            {
                return ObjectHandle.DateTime;
            }
            else if (handleType == typeof(Guid))
            {
                return ObjectHandle.Guid;
            }
            else if (handleType == typeof(TimeSpan))
            {
                return ObjectHandle.TimeSpan;
            }
            else if (handleType == typeof(decimal))
            {
                return ObjectHandle.Decimal;
            }
            else if (handleType.IsArray)
            {
                if (handleType.GetElementType() == typeof(byte))
                    return ObjectHandle.ByteArray;
                return ObjectHandle.Array;
            }
            else if ((typeof(IList).IsAssignableFrom(handleType)))
            {
                if (handleType.IsGenericType)
                    return ObjectHandle.ListGeneric;
                return ObjectHandle.List;
            }
            else if (handleType.IsEnum)
            {
                return ObjectHandle.Enum;
            }
            return ObjectHandle.Unknown;
        }
    }

    public enum ObjectHandle
    {
        Null,
        DBNull,
        Zero,
        Boolean,
        Byte,
        Char,
        Decimal,
        Double,
        Int16,
        Int32,
        Int64,
        SByte,
        Single,
        String,
        UInt16,
        UInt32,
        UInt64,
        Array,
        List,
        ListGeneric,
        ByteArray,
        TimeSpan,
        DateTime,
        Guid,
        Enum,
        Unknown
    }
}
